// FBO.h
//
// Assumes gl.h already included
#pragma once

namespace OpenGL {

class FBO
{
protected:
	struct ColorAttachment
	{
		GLenum attachmentType;		// GL_TEXTURE_2D, GL_DEPTH_COMPONENT...
		GLenum attachmentFormat;	// GL_RGBA
		GLuint attachmentID;		// texture ID
	};

	int width;
	int height;
	bool bound;

	GLuint fbo;
	ColorAttachment attachments[16];
	GLuint numAttachments;

	//GLenum colorFormat;
	//GLuint colorTexture;

	GLenum depthFormat;
	GLuint depthBuffer;

	// ------------------------------------------------------------------------------------------------
	void init()
	{
		width				= 0;
		height				= 0;
		fbo					= 0;
		numAttachments		= 0;
		//colorFormat			= GL_NONE;
		depthFormat			= GL_NONE;
		//colorTexture		= 0;
		//depthBuffer			= 0;
		bound				= false;
	}

	// ------------------------------------------------------------------------------------------------
	void copy(const FBO &s)
	{
		(void)s;
		destroy(true);
	}

	// ------------------------------------------------------------------------------------------------
	GLenum checkFBO()
	{
		GLenum ret = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER);
		switch( ret )
		{
		case GL_FRAMEBUFFER_COMPLETE: break;
		case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:			printf("FBO Failed: Attachment Error\n"); break;
		case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:	printf("FBO Failed: Missing attachment\n");  break;
		//case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:			printf("FBO Failed: Dimensions Error\n"); break;
		//case GL_FRAMEBUFFER_INCOMPLETE_FORMATS:				printf("FBO Failed: Formats Error\n"); break;
		case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER:			printf("FBO Failed: Draw buffer Error\n"); break;
		case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER:			printf("FBO Failed: Read buffer Error\n"); break;
		case GL_FRAMEBUFFER_UNSUPPORTED:					printf("FBO Failed: Not Supported!\n"); break;
		default: printf("FBO Failed: Unknown Error\n"); break;
		}

		return ret;
	}


public:
	static bool isSupported()
	{
		return glewIsSupported("GL_ARB_framebuffer_object") || glewIsSupported("GL_EXT_framebuffer_object");
	}

	// ------------------------------------------------------------------------------------------------
	FBO()				{init();}
	// ------------------------------------------------------------------------------------------------
	FBO(const FBO &s)	{ init(); copy(s); }
	// ------------------------------------------------------------------------------------------------
	virtual ~FBO()		{ destroy(true); }

	// ------------------------------------------------------------------------------------------------
	bool create(int w, int h)
	{
		destroy(true);
	
		// Create the frame buffer
		glGenFramebuffersEXT(1, &fbo);
		//glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
		width  = w;
		height = h;
		return true;

	}

	// ------------------------------------------------------------------------------------------------
	// create a FBO with texture as attachments
	bool create(int w, int h, GLenum colorformat, GLenum depthformat, bool generateMipmap = false)
	{
		// already created ?
		//if( isValid() )
		//{
		//	if( width == w && 
		//		height == h && 
		//		colorformat == attachments[0].attachmentFormat &&
		//		depthFormat == depthformat)
		//	{
		//		// non need to create
		//		return true;
		//	}
		//}

		destroy(true);

		// invalid parameters
		if( w <= 0 || h <= 0 )
			return false;

		// Create empty color texture ?
		if( colorformat != GL_NONE )
		{
			attachments[0].attachmentType   = GL_TEXTURE_2D;
			attachments[0].attachmentFormat = GL_RGBA;

			glGenTextures(1, &attachments[0].attachmentID);
			glBindTexture(attachments[0].attachmentType, attachments[0].attachmentID);
			glTexParameterf(attachments[0].attachmentType, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
			glTexParameterf(attachments[0].attachmentType, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

			if( generateMipmap )
			{
				glTexParameteri(attachments[0].attachmentType, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
				glTexParameteri(attachments[0].attachmentType, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
				glGenerateMipmap(attachments[0].attachmentType);
			}
			else
			{
				glTexParameterf(attachments[0].attachmentType, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
				glTexParameterf(attachments[0].attachmentType, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
			}
			glTexImage2D(attachments[0].attachmentType, 0, attachments[0].attachmentFormat, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
			glBindTexture(attachments[0].attachmentType, 0);
			
			numAttachments = 1;
		}

		// Want a depth attachment?
		if( depthformat != GL_NONE )
		{
			//glGenRenderbuffersEXT(1, &depthBuffer);
			//glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, depthBuffer);
			//glRenderbufferStorage(GL_RENDERBUFFER_EXT, depthformat, w, h);
			//glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0);

			//// attach depth
			//glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBuffer);


			glGenTextures(1, &depthBuffer);
			glBindTexture(GL_TEXTURE_2D,depthBuffer);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
			glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
			//glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
			//glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE_ARB, GL_LUMINANCE);
			//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE_ARB, GL_COMPARE_R_TO_TEXTURE_ARB);
			//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);
			glTexImage2D(GL_TEXTURE_2D, 0, depthformat, w, h, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
			glBindTexture(GL_TEXTURE_2D, 0);
		}

		// Create the frame buffer
		glGenFramebuffersEXT(1, &fbo);
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
		//glCheck;

		// attach color
		if( numAttachments == 1 )
			glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, attachments[0].attachmentType, attachments[0].attachmentID, 0);
		//glCheck;

		// attach depth
		if( depthBuffer != 0 )
		{
			/*GLuint rboId;
			glGenRenderbuffers(1, &rboId);
			glBindRenderbuffer(GL_RENDERBUFFER, rboId);
			glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height);
			glBindRenderbuffer(GL_RENDERBUFFER, 0);*/

			glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthBuffer, 0);
		}

		// check for any error
		const GLenum s = checkFBO();
		const bool res = (s == GL_FRAMEBUFFER_COMPLETE);

		// Bind no FBO
		glBindFramebufferEXT(GL_FRAMEBUFFER, 0);

		if(!res)
		{
			glDeleteFramebuffersEXT(1, &fbo);
			glDeleteTextures(1, &attachments[0].attachmentID);

			if(depthBuffer != 0)
				//glDeleteRenderbuffers(1, &depthBuffer);
				glDeleteTextures(1, &depthBuffer);
				

			fbo							= 0;
			attachments[0].attachmentID = 0;
			depthBuffer					= 0;
			numAttachments				= 0;

			return false;
		}

		width		= w;
		height		= h;
		//colorFormat = colorformat;
		depthFormat = depthformat;

		return true;
	}

	// ------------------------------------------------------------------------------------------------
	void destroy(bool destroyAttachments)
	{
		if( !isValid() ) return;

		glDeleteFramebuffersEXT(1, &fbo);

		if(destroyAttachments)
		{
			for( GLuint i=0; i<numAttachments; ++i)
			{
				if(attachments[i].attachmentType == GL_RENDERBUFFER)
					glDeleteRenderbuffersEXT( 1,&attachments[i].attachmentID );
				else
					glDeleteTextures(1, &attachments[i].attachmentID );
			}

			if(depthBuffer != 0)
				//glDeleteRenderbuffers(1, &depthBuffer);
				glDeleteTextures(1, &depthBuffer);
		}

		init();
	}

	// ------------------------------------------------------------------------------------------------
	bool isValid() const	{return fbo != 0;}

	// ------------------------------------------------------------------------------------------------
	int getWidth() const	{return width;}

	// ------------------------------------------------------------------------------------------------
	int getHeight() const	{return height;}

	// ------------------------------------------------------------------------------------------------
	//GLenum getColorFormat() const	{return colorFormat;}

	// ------------------------------------------------------------------------------------------------
	GLenum getDepthFormat() const	{return depthFormat;}

	// ------------------------------------------------------------------------------------------------
	void bindColor(unsigned int i)	{if(i<numAttachments) glBindTexture(attachments[i].attachmentType, attachments[i].attachmentID);}

	// ------------------------------------------------------------------------------------------------
	void bindDepth()	{glBindTexture(GL_TEXTURE_2D, depthBuffer);}

	// ------------------------------------------------------------------------------------------------
	void bind()
	{
		if( !isValid() )
			return;

		//glGetIntegerv(GL_DRAW_BUFFER, &drawBufferBackup);

		glBindFramebufferEXT(GL_FRAMEBUFFER, fbo);
		glPushAttrib(GL_VIEWPORT_BIT);
		glViewport(0, 0, width, height);
		bound = true;
	}

	// ------------------------------------------------------------------------------------------------
	void clear(float r = 0.f, float g = 0.f, float b = 0.f, float a = 1.f)
	{
		if(bound)
		{
			glClearColor(r,g,b,a);
			glClear(GL_COLOR_BUFFER_BIT | (depthFormat != GL_NONE ? GL_DEPTH_BUFFER_BIT : 0) );	
		}
	}

	// ------------------------------------------------------------------------------------------------
	void unBind()
	{
		glBindFramebufferEXT(GL_FRAMEBUFFER, 0); 
		
		if( bound )
		{
			//glDrawBuffer(drawBufferBackup);
			glPopAttrib();
			bound = false;
		}
	}

	// ------------------------------------------------------------------------------------------------
	void unBindAttachment() {glBindTexture(GL_TEXTURE_2D, 0);}

	// ------------------------------------------------------------------------------------------------
	void readColor(unsigned int attachPoint, GLenum format, GLenum type, void *pixels)
	{
		if( attachPoint > numAttachments )
			return;

		glBindTexture(attachments[attachPoint].attachmentType, attachments[attachPoint].attachmentID);
		glGetTexImage(attachments[attachPoint].attachmentType, 0, format, type, pixels);
		glBindTexture(attachments[attachPoint].attachmentType, 0);
	}

	// ------------------------------------------------------------------------------------------------
	void readDepth( float *pixels )
	{
		glBindFramebufferEXT(GL_FRAMEBUFFER, fbo);
		glReadPixels(0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, pixels);
		glBindFramebufferEXT(GL_FRAMEBUFFER, 0);
	}

	// ------------------------------------------------------------------------------------------------
	FBO & operator = (const FBO & s)
	{
		copy(s);
		return (*this);
	}

	// ------------------------------------------------------------------------------------------------
	void addAttachment(GLenum type, GLenum format, GLuint targetID)
	{
		if( !isValid() )
			return;

		glBindFramebufferEXT(GL_FRAMEBUFFER, fbo);

		// attach color
		if( type != GL_RENDERBUFFER )
			glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0+numAttachments, type, targetID, 0);
		else
			glFramebufferRenderbufferEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0+numAttachments,GL_RENDERBUFFER, targetID);

		//GLenum *drawBuffers = new GLenum[numAttachments+1];
		//for(int i=0;i<numAttachments+1; ++i)
		//{
		//	drawBuffers[i] = GL_COLOR_ATTACHMENT0+i;
		//}

		//glDrawBuffers(numAttachments+1, drawBuffers);
		//delete [] drawBuffers;
		// check for any error
		const GLenum s = checkFBO();
		const bool res = (s == GL_FRAMEBUFFER_COMPLETE);

		// Bind no FBO
		glBindFramebufferEXT(GL_FRAMEBUFFER, 0);
		

		if( res )
		{
			attachments[numAttachments].attachmentType   = type;
			attachments[numAttachments].attachmentFormat = format;
			attachments[numAttachments].attachmentID     = targetID;
			
			numAttachments++;
		}
		glCheck;
	}

	// ------------------------------------------------------------------------------------------------
	void removeAttachment()
	{
		numAttachments--;
		glFramebufferTexture2DEXT(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0+numAttachments, attachments[numAttachments].attachmentType, 0, 0);
		
		//GLenum *drawBuffers = new GLenum[numAttachments+1];
		//for(int i=0;i<numAttachments+1; ++i)
		//{
		//	drawBuffers[i] = GL_COLOR_ATTACHMENT0+i;
		//}

		//glDrawBuffers(numAttachments+1, drawBuffers);
		//delete [] drawBuffers;
	}

	// ------------------------------------------------------------------------------------------------
	void setDrawBuffers(int num, GLenum buffers[])
	{
		glBindFramebufferEXT(GL_FRAMEBUFFER, fbo);
		
		if( num == 0 )	glDrawBuffer( GL_NONE );
		else			glDrawBuffers( num, buffers );

		glBindFramebufferEXT(GL_FRAMEBUFFER, 0);	
	}

	// ------------------------------------------------------------------------------------------------
	void setReadBuffer(GLenum buffer)
	{
		glBindFramebufferEXT(GL_FRAMEBUFFER, fbo);
		glReadBuffer(buffer);
		glBindFramebufferEXT(GL_FRAMEBUFFER, 0);
	}
};

}}